/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.remote;

import java.awt.*;
import java.awt.image.renderable.ParameterBlock;
import java.net.InetAddress;
import java.net.URL;
import java.rmi.Naming;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
import org.eclipse.imagen.*;
import org.eclipse.imagen.media.rmi.ImageServer;
import org.eclipse.imagen.media.rmi.JAIRMIUtil;
import org.eclipse.imagen.media.rmi.RMIServerProxy;
import org.eclipse.imagen.media.serialize.SerializableState;
import org.eclipse.imagen.media.serialize.SerializerFactory;
import org.eclipse.imagen.util.CaselessStringKey;
import org.eclipse.imagen.util.ImagingException;
import org.eclipse.imagen.util.ImagingListener;

/**
 * This class describes the "jairmi" remote imaging protocol. This protocol assumes that both the client and the server
 * are running JAI. The communication between the client and the server takes place using the Remote Method Invocation
 * (RMI) mechanism.
 *
 * <p>In order to locate the "jairmi" server, a RMI registry must be running on this server, and the "jairmi" server
 * must have registered itself with this RMI registry by binding itself under the <code>IMAGE_SERVER_BIND_NAME</code>
 * <code>String</code>. The RMI registry is a simple remote object name service that allows remote clients to get a
 * reference to a remote object by name.
 *
 * <p>The "jairmi" protocol expects the <code>String</code> that represents the server to be a <code>URL</code>
 * formatted <code>String</code> of the form:
 *
 * <pre>
 * //host:port
 * </pre>
 *
 * where <code>host</code> is the name, or IP address of the "jairmi" remote imaging server, and <code>port</code> is
 * the port number where a rmiregistry is running on the same host. A protocol like "rmi:" does not need to be included
 * in this URL formatted <code>String</code>. If the serverName <code>String</code> is null, the local host is used as a
 * default. If the port is not included in the serverName <code>String</code>, it defaults to the well-known port for
 * rmiregistry, 1099.
 *
 * <p>If the serverName supplied to any "jairmi" protocol implementing class's method is null, then the local host will
 * be used instead.
 *
 * <p>The default "jairmi" server provided with JAI is <code>org.eclipse.imagen.media.rmi.JAIRMIRemoteServer</code>.
 * This server can be run in the following manner, after starting a rmiregistry on the host where the server will be
 * run:
 *
 * <pre>
 * java -Djava.rmi.server.codebase="file:$JAI/lib/jai_core.jar file:$JAI/lib/jai_codec.jar" -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=file:$JAI/policy org.eclipse.imagen.media.rmi.JAIRMIImageServer
 * </pre>
 *
 * where $JAI refers to the directory where JAI is installed. This server binds itself with the running rmiregistry
 * under the <code>IMAGE_SERVER_BIND_NAME</code> <code>String</code> bind name, and can be used to serve "jairmi"
 * requests. The policy file specified above needs to be created by the user. Information on policy files and
 * permissions can be found at
 *
 * <p>http://java.sun.com/j2se/1.3/docs/guide/security/PolicyFiles.html
 *
 * <p>http://java.sun.com/j2se/1.3/docs/guide/security/permissions.html
 *
 * <p>The JAI instance used by the "jairmi" remote imaging server can be configured by providing an implementation of
 * the <code>org.eclipse.imagen.media.remote.JAIServerConfigurationSpi</code> interface on the <code>CLASSPATH</code>
 * when starting the server. For more details, please refer to
 * {@link org.eclipse.imagen.media.remote.JAIServerConfigurationSpi}
 *
 * <p>The "jairmi" remote imaging server supports the following configurable parameters whose values can be specified on
 * the command line when starting the server : <code>
 *        -host <string> The server name or server IP address
 *        -port <integer> The port that rmiregistry is running on
 *        -rmiRegistryPort <integer> Same as -port option
 *        -serverPort <integer> The port that the server should listen on,
 *                           for connections from clients
 *        -cacheMemCapacity <long> The memory capacity in bytes.
 *        -cacheMemThreshold <float> The memory threshold, which is the
 *                           fractional amount of cache memory to
 *                           retain during tile removal
 *        -disableDefaultCache Disable use of default tile cache.
 *        -schedulerParallelism <integer> The degree of parallelism of the
 *                           default TileScheduler
 *        -schedulerPrefetchParallelism <integer> The degree of parallelism
 *                           of the default TileScheduler for tile prefetching
 *        -schedulerPriority <integer> The priority of tile scheduling for
 *                           the default TileScheduler
 *        -schedulerPrefetchPriority <integer> The priority of tile prefetch
 *                           scheduling for the default TileScheduler
 *        -defaultTileSize <integer>x<integer> The default tile dimensions in
 *                           the form <xSize>x<ySize>
 *        -defaultRenderingSize <integer>x<integer> The default size to render
 *                           a RenderableImage to, in the form <xSize>x<ySize>
 *        -serializeDeepCopy <boolean> Whether a deep copy of the image data
 *                           should be used when serializing images
 *        -tileCodecFormat <string> The default format to be used for tile
 *                           serialization via TileCodecs
 *        -retryInterval <integer> The retry interval value to be used for
 *                           dealing with network errors during remote imaging
 *        -numRetries <integer> The number of retries to be used for dealing
 *                           with network errors during remote imaging
 * </code>
 *
 * <p>It should be noted that if a parameter was set via JAIServerConfigurationSpi, and the command line option for the
 * same parameter specifies a different value, then the command line specified parameter value will be honored. That is
 * to say that the JAIServerConfigurationSpi specified configuration happens first, followed by command line parameter
 * configuration, and the last configuration to be applied overwrites all previous settings.
 *
 * @since JAI 1.1
 */
public class JAIRMIDescriptor extends RemoteDescriptorImpl {

    /**
     * The bind name for the remote "jairmi" server. This is also the name that the "jairmi" client looks for when
     * trying to locate a "jairmi" server.
     */
    public static final String IMAGE_SERVER_BIND_NAME = "JAIRMIRemoteServer1.1";

    // A MessageFormat object to format the error strings.
    private MessageFormat formatter;

    /** Creates a <code>JAIRMIDescriptor</code>. */
    public JAIRMIDescriptor() throws java.net.MalformedURLException {
        super(
                "jairmi",
                new URL(
                        "http://java.sun.com/products/java-media/jai/forDevelopers/jai-apidocs/javax/media/jai/remote/JAIRMIDescriptor.html"));

        formatter = new MessageFormat("");
        formatter.setLocale(Locale.getDefault());
    }

    /**
     * Returns the list of <code>OperationDescriptor</code>s that describe the operations supported by the server. It is
     * the implementing class's responsibility to extract this information from either the server or from its own
     * knowledge of the remote imaging protocol. The "jairmi" protocol gets this information from the server.
     *
     * <p>If the supplied serverName argument is null, then the local host will be used instead.
     *
     * @param serverName The <code>String</code> identifying the server.
     */
    public OperationDescriptor[] getServerSupportedOperationList(String serverName) throws RemoteImagingException {

        List odList = null;
        try {
            odList = getImageServer(serverName).getOperationDescriptors();
        } catch (Exception e) {
            sendExceptionToListener(
                    JaiI18N.getString("JAIRMIDescriptor12"),
                    new RemoteImagingException(JaiI18N.getString("JAIRMIDescriptor12"), e));
            //	    throw new RemoteImagingException(ImageUtil.getStackTraceString(e));
        }

        OperationDescriptor[] od = new OperationDescriptor[odList.size()];
        int count = 0;
        for (Iterator i = odList.iterator(); i.hasNext(); ) {
            od[count++] = (OperationDescriptor) i.next();
        }

        return od;
    }

    private ImageServer getImageServer(String serverName) {

        if (serverName == null) {
            try {
                serverName = InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) {
                sendExceptionToListener(
                        JaiI18N.getString("JAIRMIDescriptor13"),
                        new ImagingException(JaiI18N.getString("JAIRMIDescriptor13"), e));
                //		throw new RuntimeException(e.getMessage());
            }
        }

        // Derive the service name.
        String serviceName = new String("rmi://" + serverName + "/" + IMAGE_SERVER_BIND_NAME);

        ImageServer imageServer = null;
        // Look up the remote object.
        try {
            imageServer = (ImageServer) Naming.lookup(serviceName);
        } catch (Exception e) {
            sendExceptionToListener(
                    JaiI18N.getString("JAIRMIDescriptor14"),
                    new RemoteImagingException(JaiI18N.getString("JAIRMIDescriptor14"), e));
            //	    throw new RemoteImagingException(ImageUtil.getStackTraceString(e));
        }

        return imageServer;
    }

    /**
     * Returns the set of capabilites supported by the server. It is the implementing class's responsibility to extract
     * this information from either the server or from its own knowledge of the remote imaging protocol. The "jairmi"
     * protocol gets this information from the server.
     *
     * <p>If the supplied serverName argument is null, then the local host will be used instead.
     *
     * @param serverName The <code>String</code> identifying the server.
     */
    public NegotiableCapabilitySet getServerCapabilities(String serverName) throws RemoteImagingException {

        NegotiableCapabilitySet serverCapabilities = null;
        try {
            serverCapabilities = getImageServer(serverName).getServerCapabilities();
        } catch (Exception e) {
            sendExceptionToListener(
                    JaiI18N.getString("JAIRMIDescriptor15"),
                    new RemoteImagingException(JaiI18N.getString("JAIRMIDescriptor15"), e));
            //	    throw new RemoteImagingException(ImageUtil.getStackTraceString(e));
        }

        return serverCapabilities;
    }

    /**
     * Calculates the region over which two distinct remote renderings of an operation may be expected to differ. The
     * operation is represented by the <code>OperationNode</code> argument to this method. The <code>String</code> that
     * identifies the operation can be retrieved via the <code>OperationNode</code>'s <code>getOperationName()</code>
     * method.
     *
     * <p>The class of the returned object will vary as a function of the nature of the operation. For rendered and
     * renderable two- dimensional images this should be an instance of a class which implements <code>java.awt.Shape
     * </code>.
     *
     * @param registryModeName The name of the mode.
     * @param oldServerName The previous server name.
     * @param oldParamBlock The previous sources and parameters.
     * @param oldHints The previous hints.
     * @param newServerName The current server name.
     * @param newParamBlock The current sources and parameters.
     * @param newHints The current hints.
     * @param node The affected node in the processing chain.
     * @return The region over which the data of two renderings of this operation may be expected to be invalid or
     *     <code>null</code> if there is no common region of validity. If an empty <code>java.awt.Shape</code> is
     *     returned, this indicates that all pixels within the bounds of the old rendering remain valid.
     * @throws IllegalArgumentException if <code>registryModeName</code> is <code>null</code> or if the operation
     *     requires either sources or parameters and either <code>oldParamBlock</code> or <code>newParamBlock</code> is
     *     <code>null</code>.
     * @throws IllegalArgumentException if there is no OperationDescriptor for the specified operationName on any one or
     *     both of the servers identified by <code>oldServerName</code> and <code>newServerName</code>, or if the number
     *     of sources or the name, number and <code>Class</code> of the operation's parameters is not the same on both
     *     the servers.
     * @throws IllegalArgumentException if <code>oldParamBlock</code> or <code>newParamBlock</code> do not contain
     *     sufficient sources or parameters for the operation in question.
     */
    public Object getInvalidRegion(
            String registryModeName,
            String oldServerName,
            ParameterBlock oldParamBlock,
            RenderingHints oldHints,
            String newServerName,
            ParameterBlock newParamBlock,
            RenderingHints newHints,
            OperationNode node)
            throws RemoteImagingException {

        if (registryModeName == null) throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor11"));

        String operationName = node.getOperationName();
        OperationDescriptor oldDescs[] = getServerSupportedOperationList(oldServerName);
        OperationDescriptor oldOD = getOperationDescriptor(oldDescs, operationName);

        if (oldOD == null) throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor1"));

        int numSources = oldOD.getNumSources();

        // If the supplied registryModeName is "remoteRendered" or
        // "remoteRenderable", in order to get the OperationDescriptor's
        // ParameterListDescriptor, we need to actually use the "rendered"
        // or "renderable" mode respectively.
        ParameterListDescriptor oldPLD = null;
        if (registryModeName.equalsIgnoreCase("remoteRendered")) {
            oldPLD = oldOD.getParameterListDescriptor("rendered");
        } else if (registryModeName.equalsIgnoreCase("remoteRenderable")) {
            oldPLD = oldOD.getParameterListDescriptor("renderable");
        } else {
            oldPLD = oldOD.getParameterListDescriptor(registryModeName);
        }

        int numParams = oldPLD.getNumParameters();

        // If the serverNames are same, nothing to be done for that
        if (oldServerName != newServerName) {

            // Check whether they both support the supplied operation name

            OperationDescriptor newDescs[] = getServerSupportedOperationList(newServerName);
            OperationDescriptor newOD;

            if ((newOD = getOperationDescriptor(newDescs, operationName)) == null)
                throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor2"));

            // Check the OperationDescriptor equivalence

            // Sources
            if (numSources != newOD.getNumSources())
                throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor3"));

            // Parameters
            ParameterListDescriptor newPLD = newOD.getParameterListDescriptor(registryModeName);

            if (numParams != newPLD.getNumParameters())
                throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor4"));

            // Param names
            String oldParamNames[] = oldPLD.getParamNames();
            if (oldParamNames == null) oldParamNames = new String[0];
            String newParamNames[] = newPLD.getParamNames();
            if (newParamNames == null) newParamNames = new String[0];

            Hashtable oldHash = hashNames(oldParamNames);
            Hashtable newHash = hashNames(newParamNames);

            // The same names should be present in both in the same order.
            if (containsAll(oldHash, newHash) == false)
                throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor8"));

            // Param class types
            Class thisParamClasses[] = oldPLD.getParamClasses();
            Class otherParamClasses[] = newPLD.getParamClasses();
            for (int i = 0; i < oldParamNames.length; i++) {
                if (thisParamClasses[i] != otherParamClasses[getIndex(newHash, oldParamNames[i])])
                    throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor9"));
            }

            // XXX Could be made more efficient by returning the area that
            // might be valid if both the servers support the same operations,
            // current implementation just returns null.
            return null;
        }

        // Perform the other checks listed in the method spec

        // Neither the old and the new ParamBlock should be null, if
        // the operation requires some sources or some parameters.
        if ((registryModeName == null)
                || ((numSources > 0 || numParams > 0) && (oldParamBlock == null || newParamBlock == null))) {
            throw new IllegalArgumentException(JaiI18N.getString("JAIRMIDescriptor5"));
        }

        // Both the old and new ParameterBlock should contain the
        // required number of sources.
        if ((numSources > 0)
                && (oldParamBlock.getNumSources() != numSources || newParamBlock.getNumSources() != numSources)) {
            Object[] msgArg0 = {operationName, new Integer(numParams)};
            formatter.applyPattern(JaiI18N.getString("JAIRMIDescriptor6"));
            throw new IllegalArgumentException(formatter.format(msgArg0));
        }

        // Both the old and new ParameterBlock should contain the
        // required number of parameters.
        if ((numParams > 0)
                && (oldParamBlock.getNumParameters() != numParams || newParamBlock.getNumParameters() != numParams)) {
            Object[] msgArg0 = {operationName, new Integer(numParams)};
            formatter.applyPattern(JaiI18N.getString("JAIRMIDescriptor7"));
            throw new IllegalArgumentException(formatter.format(msgArg0));
        }

        // Find the id that refers to the corresponding RenderedOp on the
        // server
        RenderedOp op = (RenderedOp) node;
        Object rendering = op.getRendering();
        Long id = null;
        if (rendering instanceof RMIServerProxy) {
            id = ((RMIServerProxy) rendering).getRMIID();
        } else {
            throw new RuntimeException(JaiI18N.getString("JAIRMIDescriptor10"));
        }

        // Check whether any of the sources of this operation are on
        // remote "jairmi" servers and if so, replace the source with
        // it's id.

        boolean samePBs = false;
        if (oldParamBlock == newParamBlock) samePBs = true;

        Vector oldSources = oldParamBlock.getSources();
        oldParamBlock.removeSources();
        // Ensure that any images which are parameters are replaced byte
        // suitable representations
        JAIRMIUtil.checkClientParameters(oldParamBlock, oldServerName);
        oldParamBlock.setSources(JAIRMIUtil.replaceSourcesWithId(oldSources, oldServerName));

        if (samePBs) {
            newParamBlock = oldParamBlock;
        } else {
            Vector newSources = newParamBlock.getSources();
            newParamBlock.removeSources();
            // Ensure that any images which are parameters are replaced byte
            // suitable representations
            JAIRMIUtil.checkClientParameters(newParamBlock, oldServerName);
            newParamBlock.setSources(JAIRMIUtil.replaceSourcesWithId(newSources, oldServerName));
        }

        // Serialize the old and new RenderingHints
        SerializableState oldRHS = SerializerFactory.getState(oldHints, null);
        SerializableState newRHS = SerializerFactory.getState(newHints, null);

        SerializableState shapeState = null;
        try {
            shapeState =
                    getImageServer(oldServerName).getInvalidRegion(id, oldParamBlock, oldRHS, newParamBlock, newRHS);
        } catch (Exception e) {
            sendExceptionToListener(
                    JaiI18N.getString("JAIRMIDescriptor16"),
                    new RemoteImagingException(JaiI18N.getString("JAIRMIDescriptor16"), e));
            //	    throw new RemoteImagingException(ImageUtil.getStackTraceString(e));
        }

        return shapeState.getObject();
    }

    private Hashtable hashNames(String paramNames[]) {

        Hashtable h = new Hashtable();
        if (paramNames != null) {
            for (int i = 0; i < paramNames.length; i++) {
                h.put(new CaselessStringKey(paramNames[i]), new Integer(i));
            }
        }

        return h;
    }

    private int getIndex(Hashtable h, String s) {
        return ((Integer) h.get(new CaselessStringKey(s))).intValue();
    }

    // A case insensitive containsAll for Hashtables containing Strings
    private boolean containsAll(Hashtable thisHash, Hashtable otherHash) {

        CaselessStringKey thisNameKey;
        for (Enumeration i = thisHash.keys(); i.hasMoreElements(); ) {
            thisNameKey = (CaselessStringKey) i.nextElement();
            if (otherHash.containsKey(thisNameKey) == false) return false;
        }

        return true;
    }

    private OperationDescriptor getOperationDescriptor(OperationDescriptor descriptors[], String operationName) {

        OperationDescriptor od;
        for (int i = 0; i < descriptors.length; i++) {
            od = descriptors[i];
            if (od.getName().equalsIgnoreCase(operationName)) return od;
        }

        return null;
    }

    void sendExceptionToListener(String message, Exception e) {
        ImagingListener listener = JAI.getDefaultInstance().getImagingListener();
        listener.errorOccurred(message, e, this, false);
    }
}
